Capítulo 4: A Estrutura da Interface

Imediatamente após a conclusão dos testes do TeleportModule, o foco muda para a construção da interface do usuário. A funcionalidade de mover o personagem estava robusta e validada; agora era necessário dar ao usuário uma forma limpa, elegante e intuitiva de interagir com o sistema.

A UI não era um detalhe lateral; era a manifestação da qualidade do projeto. As diretrizes eram claras: design minimalista, Dark/Light, cantos arredondados, e arquitetura de painel flutuante de tamanho fixo. O módulo de teleporte dependia do DataModule para funcionalidade; o Módulo UI, por sua vez, seria o orquestrador que conectaria tudo, expondo a funcionalidade ao usuário.

Módulo UI: A Arquitetura do Painel

O ponto de partida era a organização visual. A interface seria construída atomicamente, começando pelo container principal e seus blocos estruturais: o cabeçalho (Header), a área de controle (ControlArea), e a lista de waypoints (WaypointList). Essa hierarquia garantiria a clareza visual e a separação de responsabilidades.

O Módulo UI seria responsável por instanciar os elementos visuais, configurar seus estilos e, crucialmente, gerenciar os estados de interação do sistema: qual modo de teleporte está ativo, se um teleporte está em andamento, e o que deve ser exibido.

local UIManager = {} -- Configurações visuais globais local GlobalStyle = { Size = UDim2.new(0, 350, 0, 550), -- Tamanho absolutamente fixo (Largura: 350px, Altura: 550px) BackgroundColor = Color3.fromRGB(30, 32, 40), AccentColor = Color3.fromRGB(70, 72, 80), TextColor = Color3.fromRGB(240, 240, 240), Transparency = 0.05, CornerRadius = 8, Padding = 12, -- Espaçamento interno padrão HeaderHeight = 40, Font = Enum.Font.SourceSans } -- Containers da UI local MainFrame = nil local Header = nil local ControlArea = nil local WaypointListContainer = nil

Estrutura e Estilização

A criação do ScreenGui e do MainFrame seguiu os padrões visuais exigidos. O Módulo UI injetaria o ScreenGui diretamente no CoreGui do cliente, posicionando o painel de forma discreta.

O MainFrame precisava ser o coração fixo da interface.

local function CreateMainFrame() -- 1. ScreenGui local Screen = Instance.new("ScreenGui") Screen.Name = "WaypointSystemUI" Screen.IgnoreGuiInset = true -- Ocupar toda a tela, ignorando a barra superior do Roblox Screen.DisplayOrder = 99 -- Garantir que fique acima de outras UIs Screen.Parent = Players.LocalPlayer:WaitForChild("PlayerGui") -- 2. MainFrame (O Painel) MainFrame = Instance.new("Frame") MainFrame.Name = "MainPanel" MainFrame.Size = GlobalStyle.Size -- Posição inicial: Canto superior esquerdo com offset visual confortável MainFrame.Position = UDim2.new(0, GlobalStyle.Padding * 2, 0, GlobalStyle.Padding * 2) MainFrame.BackgroundColor3 = GlobalStyle.BackgroundColor MainFrame.BackgroundTransparency = GlobalStyle.Transparency MainFrame.BorderSizePixel = 0 -- Borda arredondada (UIStroke e UICorner) local Corner = Instance.new("UICorner") Corner.CornerRadius = UDim.new(0, GlobalStyle.CornerRadius) Corner.Parent = MainFrame local Stroke = Instance.new("UIStroke") Stroke.Color = GlobalStyle.AccentColor Stroke.Thickness = 1 Stroke.Transparency = 0.5 Stroke.Parent = MainFrame MainFrame.Parent = Screen end

A decisão de arquitetura de usar UIStroke e UICorner em vez de apenas ajustar a imagem do Frame reforçava a estética limpa e nativa.

O Header (Cabeçalho)

O Header era crucial. Seria ele a âncora para o mecanismo de arrastar e o local para o título. Sua altura era fixa (GlobalStyle.HeaderHeight).

local function CreateHeader() Header = Instance.new("Frame") Header.Name = "Header" Header.Size = UDim2.new(1, 0, 0, GlobalStyle.HeaderHeight) Header.Position = UDim2.new(0, 0, 0, 0) Header.BackgroundColor3 = GlobalStyle.AccentColor Header.BackgroundTransparency = 0.9 -- Levemente mais escuro que o painel principal, mas transparente Header.BorderSizePixel = 0 Header.Parent = MainFrame -- Padding Horizontal local PaddingFrame = Instance.new("Frame") PaddingFrame.Name = "HeaderPadding" PaddingFrame.Size = UDim2.new(1, -GlobalStyle.Padding * 2, 1, 0) PaddingFrame.Position = UDim2.new(0, GlobalStyle.Padding, 0, 0) PaddingFrame.BackgroundTransparency = 1 PaddingFrame.Parent = Header -- Título local Title = Instance.new("TextLabel") Title.BackgroundTransparency = 1 Title.Size = UDim2.new(1, 0, 1, 0) Title.Font = GlobalStyle.Font Title.TextSize = 16 Title.TextColor3 = GlobalStyle.TextColor Title.Text = "Waypoint System" Title.TextXAlignment = Enum.TextXAlignment.Left Title.Parent = PaddingFrame end

Arrastabilidade: O Mínimo Essencial de Interação

A funcionalidade de arrastar era fundamental para a usabilidade. O usuário deveria poder mover o painel para uma área da tela que não interferisse no jogo, mas o movimento precisava ser suave — nada de saltos ou instabilidade.

A lógica de arrasto seria anexada ao Header, pois era a área mais óbvia e funcional para essa interação, liberando o corpo do painel para outros controles e a lista.

local IsDragging = false local StartPosition = nil local StartMousePosition = nil local InputService = game:GetService("UserInputService") local RunService = game:GetService("RunService") local function SetupDrag(frameToMove, dragHandle) dragHandle.InputBegan:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then IsDragging = true StartMousePosition = InputService:GetMouseLocation() StartPosition = frameToMove.Position input:Capture() -- Captura o input para garantir que o movimento não seja interrompido end end) dragHandle.InputEnded:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then IsDragging = false end end) RunService.Heartbeat:Connect(function() if IsDragging then local CurrentMousePosition = InputService:GetMouseLocation() local Delta = CurrentMousePosition - StartMousePosition -- Recalculo para manter a posição em offset absoluto (UDim2.new(0, x, 0, y)) local NewX = StartPosition.X.Offset + Delta.X local NewY = StartPosition.Y.Offset + Delta.Y -- Limitar o movimento dentro da tela (evita que o painel "escape" do campo de visão) local MaxX = frameToMove.Parent.AbsoluteSize.X - frameToMove.AbsoluteSize.X local MaxY = frameToMove.Parent.AbsoluteSize.Y - frameToMove.AbsoluteSize.Y NewX = math.clamp(NewX, 0, MaxX) NewY = math.clamp(NewY, 0, MaxY) frameToMove.Position = UDim2.new(0, NewX, 0, NewY) end end) end

A utilização do Heartbeat em vez de InputChanged para o movimento em si (InputChanged foi usado apenas para rastrear o mouse) era uma escolha de performance e suavidade. Heartbeat garante que a atualização ocorra a cada quadro, resultando em um movimento mais responsivo e menos "jittery" (instável), mesmo que o usuário vire a câmera enquanto arrasta. O movimento seria ligado ao MainFrame (frameToMove) mas ativado ao interagir com o Header (dragHandle).

Área de Controle: Inserção de Waypoints e Seleção de Modo

A ControlArea seria localizada diretamente abaixo do cabeçalho. Era aqui que o usuário iniciaria as ações — criar um novo waypoint e configurar o método de teleporte padrão.

local function CreateControlArea() ControlArea = Instance.new("Frame") ControlArea.Name = "ControlArea" -- Largura total do MainFrame (1, 0) -- Altura definida para acomodar o input e os seletores ControlArea.Size = UDim2.new(1, 0, 0, 100) ControlArea.Position = UDim2.new(0, 0, 0, GlobalStyle.HeaderHeight) ControlArea.BackgroundTransparency = 1 ControlArea.Parent = MainFrame -- Layout para organizar os elementos de controle (Padding) local Layout = Instance.new("UIListLayout") Layout.Padding = UDim.new(0, GlobalStyle.Padding) Layout.SortOrder = Enum.SortOrder.LayoutOrder Layout.Parent = ControlArea -- Padding interno em todas as direções, garantindo que o conteúdo não cole nas bordas local ControlAreaPadding = Instance.new("Frame") ControlAreaPadding.Size = UDim2.new(1, -GlobalStyle.Padding * 2, 1, -GlobalStyle.Padding) ControlAreaPadding.Position = UDim2.new(0, GlobalStyle.Padding, 0, GlobalStyle.Padding / 2) ControlAreaPadding.BackgroundTransparency = 1 ControlAreaPadding.Parent = ControlArea return ControlAreaPadding -- Retornar a área com padding para anexar componentes end

Componente 1: Criação de Waypoint

Este componente exigia um campo de texto para o nome e um botão de ação.

local WaypointCreationContainer = nil local NameInput = nil local CreateButton = nil local function CreateWaypointCreation(parentFrame) WaypointCreationContainer = Instance.new("Frame") WaypointCreationContainer.Name = "WaypointCreation" WaypointCreationContainer.Size = UDim2.new(1, 0, 0, 30) -- Altura fixa para input WaypointCreationContainer.BackgroundTransparency = 1 WaypointCreationContainer.Parent = parentFrame -- Usar Layout para organizar Input e Botão lado a lado local ListLayout = Instance.new("UIListLayout") ListLayout.FillDirection = Enum.FillDirection.Horizontal ListLayout.Padding = UDim.new(0, 5) ListLayout.Parent = WaypointCreationContainer -- Campo de Texto (Input) NameInput = Instance.new("TextBox") NameInput.Name = "NameInput" NameInput.Size = UDim2.new(0.65, 0, 1, 0) -- Ocupa 65% da largura NameInput.LayoutOrder = 1 NameInput.BackgroundColor3 = GlobalStyle.AccentColor NameInput.BackgroundTransparency = 0.7 NameInput.BorderSizePixel = 0 NameInput.Text = "New Waypoint Name" NameInput.TextSize = 14 NameInput.Font = GlobalStyle.Font NameInput.TextColor3 = GlobalStyle.TextColor NameInput.PlaceholderText = "Nome do Waypoint" NameInput.Parent = WaypointCreationContainer -- Botão Criar CreateButton = Instance.new("TextButton") CreateButton.Name = "CreateButton" CreateButton.Size = UDim2.new(0.35, -5, 1, 0) -- Ocupa o restante (35% - padding) CreateButton.LayoutOrder = 2 CreateButton.BackgroundColor3 = Color3.fromRGB(50, 150, 255) -- Cor de ação primária CreateButton.Text = "Criar WP" CreateButton.TextSize = 14 CreateButton.Font = GlobalStyle.Font CreateButton.TextColor3 = Color3.fromRGB(255, 255, 255) CreateButton.BorderSizeLineThickness = 0 CreateButton.BorderSizePixel = 0 -- Feedback visual básico (hover) CreateButton.MouseEnter:Connect(function() CreateButton:TweenSize(CreateButton.Size, Enum.EasingDirection.Out, Enum.EasingStyle.Quart, 0.1, true) CreateButton.BackgroundTransparency = 0.1 -- Fica levemente mais opaco end) CreateButton.MouseLeave:Connect(function() CreateButton.BackgroundTransparency = 0 -- Retorna à opacidade normal end) local ButtonCorner = Instance.new("UICorner") ButtonCorner.CornerRadius = UDim.new(0, GlobalStyle.CornerRadius / 2) ButtonCorner.Parent = CreateButton CreateButton.Parent = WaypointCreationContainer -- Lógica de binding (exemplo, a implementação real viria depois) CreateButton.MouseButton1Click:Connect(function() local name = NameInput.Text if name and string.len(name) > 0 and name ~= NameInput.PlaceholderText then -- DataModule.AddWaypoint(name) -- Chamada real no Módulo UI completo print("Criar Waypoint: " .. name) NameInput.Text = "" -- Limpa o input end end) end

A atenção ao detalhe estava visível no tratamento do botão. Em vez de simplesmente deixar o botão mudar de cor no hover, uma leve transparência invertida era aplicada, dando um feedback sutil e elegante. Os cantos arredondados do botão eram menores que os do painel principal para manter uma diferenciação hierárquica.

Componente 2: Seletores de Método de Teleporte

O usuário precisava de um controle claro para alternar entre "Instantâneo" e "Tween". Esse controle deveria ser coeso, visualmente destacando o estado ativo. A melhor analogia em UX era um toggle switch horizontal ou um grupo de botões ativáveis.

Usaríamos dois botões adjacentes dentro de um container, onde a cor de fundo do botão ativo mudaria drasticamente para a cor primária, enquanto o inativo permaneceria no tom de fundo.

local TeleportMethodContainer = nil local MethodButtons = {} local CurrentMethod = "instant" -- Estado interno, deve ser sincronizado com DataModule local function SetMethodVisual(method) local buttonInstant = MethodButtons["instant"] local buttonTween = MethodButtons["tween"] if method == "instant" then buttonInstant.BackgroundColor3 = Color3.fromRGB(50, 150, 255) -- Ativo (Azul) buttonInstant.TextColor3 = Color3.fromRGB(240, 240, 240) buttonTween.BackgroundColor3 = GlobalStyle.AccentColor -- Inativo (Acento) buttonTween.TextColor3 = GlobalStyle.TextColor elseif method == "tween" then buttonInstant.BackgroundColor3 = GlobalStyle.AccentColor buttonInstant.TextColor3 = GlobalStyle.TextColor buttonTween.BackgroundColor3 = Color3.fromRGB(50, 150, 255) buttonTween.TextColor3 = Color3.fromRGB(240, 240, 240) end end local function CreateMethodSelect(parentFrame) TeleportMethodContainer = Instance.new("Frame") TeleportMethodContainer.Name = "MethodSelector" TeleportMethodContainer.Size = UDim2.new(1, 0, 0, 30) TeleportMethodContainer.BackgroundTransparency = 1 TeleportMethodContainer.Parent = parentFrame local Corner = Instance.new("UICorner") Corner.CornerRadius = UDim.new(0, GlobalStyle.CornerRadius / 2) Corner.Parent = TeleportMethodContainer local ListLayout = Instance.new("UIListLayout") ListLayout.FillDirection = Enum.FillDirection.Horizontal ListLayout.Parent = TeleportMethodContainer local function CreateMethodButton(name, label, width) local button = Instance.new("TextButton") button.Name = name button.Text = label button.Size = UDim2.new(width, 0, 1, 0) button.Font = GlobalStyle.Font button.TextSize = 14 button.BorderSizePixel = 0 button.Parent = TeleportMethodContainer -- Evento de clique para alternar o estado button.MouseButton1Click:Connect(function() CurrentMethod = name SetMethodVisual(name) -- Lógica para salvar a configuração no DataModule viria aqui print("Método de teleporte alterado para: " .. name) end) MethodButtons[name] = button return button end -- Botões com pesos de largura iguais para preencher o container (0.5, 0) CreateMethodButton("instant", "Instantâneo", 0.5) CreateMethodButton("tween", "Tween (Suave)", 0.5) -- Inicializa a UI com o estado padrão (instantâneo) SetMethodVisual(CurrentMethod) end

A inclusão da função SetMethodVisual(method) era uma decisão crucial de arquitetura orientada a estados. Isso permitia que, mesmo que o estado do sistema se alterasse por um evento externo (como carregar configurações salvas), a UI pudesse ser atualizada chamando uma única função, garantindo que o estado visual refletisse o estado lógico. Os botões no toggle foram dimensionados em UDim2 para preencher exatamente 50% da largura, eliminando espaçamentos internos desnecessários e formando um painel de controle compacto e unificado.

A Lista de Waypoints: Preparação da Área Scrollável

A parte inferior do painel seria dedicada à listagem de waypoints. Esta área precisava ser scrollável se a lista excedesse o espaço vertical disponível, e deveria visualizar claramente a separação entre a área de controle e o conteúdo dinâmico.

local function CreateWaypointListContainer() WaypointListContainer = Instance.new("Frame") WaypointListContainer.Name = "WaypointListContainer" -- Posição: Abaixo da ControlArea + pequena margem local ControlAreaHeight = GlobalStyle.HeaderHeight + 100 -- 100px definidos para ControlArea WaypointListContainer.Position = UDim2.new(0, 0, 0, ControlAreaHeight + GlobalStyle.Padding) -- Altura: Restante do MainFrame local RemainingHeight = GlobalStyle.Size.Y.Offset - ControlAreaHeight - GlobalStyle.Padding * 2 WaypointListContainer.Size = UDim2.new(1, 0, 0, RemainingHeight) WaypointListContainer.BackgroundTransparency = 1 WaypointListContainer.Parent = MainFrame -- Adicionar UIGridLayout ou ListLayout, mas usar o ScrollFrame local ScrollFrame = Instance.new("ScrollingFrame") ScrollFrame.Name = "WaypointScrollFrame" ScrollFrame.Size = UDim2.new(1, 0, 1, 0) ScrollFrame.BackgroundTransparency = 1 ScrollFrame.BorderSizePixel = 0 ScrollFrame.ScrollBarThickness = 6 -- ScrollBar fina ScrollFrame.CanvasSize = UDim2.new(0, 0, 0, 0) -- Ajuste dinâmico posterior (importante) ScrollFrame.Parent = WaypointListContainer -- Layout interno para a lista de itens local ListLayout = Instance.new("UIListLayout") ListLayout.Padding = UDim.new(0, 5) ListLayout.SortOrder = Enum.SortOrder.LayoutOrder ListLayout.Parent = ScrollFrame -- Padding interno na área da lista para não colar ListLayout.SizeConstraint = Enum.SizeConstraint.RelativeXX -- Ignorar X para calcular Y pelo conteúdo ListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center -- Adicionar UIGridLayout para espaçamento e ordenação -- Não. ListLayout é suficiente e mais otimizado para listas verticais simples. Usar Center Alignment para dar padding visual. -- O ScrollFrame com ListLayout é o padrão de listas clean. return ScrollFrame end

A decisão crítica aqui era o CanvasSize. Ele foi inicializado em UDim2.new(0, 0, 0, 0). O ajuste de CanvasSize para habilitar o scroll vertical só seria implementado no próximo estágio, quando o Módulo UI começasse a gerar os itens dinamicamente, garantindo que o CanvasSize sempre se ajustasse ao tamanho total dos itens da lista (AutomaticSize em Y seria ideal, mas nem sempre funciona perfeitamente, exigindo cálculo manual no código).

Montagem e Inicialização

O processo de inicialização do Módulo UI envolvia a execução sequencial dessas funções de criação.

function UIManager.Initialize() CreateMainFrame() CreateHeader() -- Configurar arrastabilidade SetupDrag(MainFrame, Header) -- Configurar a ControlArea local ControlPaddingFrame = CreateControlArea() -- Componentes de controle CreateWaypointCreation(ControlPaddingFrame) -- Adicionar um pequeno separador visual entre a criação e a seleção de método local Separator = Instance.new("Frame") Separator.Size = UDim2.new(1, 0, 0, 1) Separator.BackgroundColor3 = GlobalStyle.AccentColor Separator.BackgroundTransparency = 0.5 Separator.Parent = ControlPaddingFrame -- Reorganizar o layout da ControlArea para incluir o separador local ControlListLayout = ControlPaddingFrame.Parent:FindFirstChildOfClass("UIListLayout") ControlListLayout.VerticalAlignment = Enum.VerticalAlignment.Top ControlListLayout.Padding = UDim.new(0, 5) -- Ajuste o padding do Layout principal -- Atualizar a área de criação e botões para respeitar o Layout WaypointCreationContainer.LayoutOrder = 1 Separator.LayoutOrder = 2 -- Criação dos seletores de método CreateMethodSelect(ControlPaddingFrame) TeleportMethodContainer.LayoutOrder = 3 -- Preparar a lista (ScrollFrame) local ScrollFrame = CreateWaypointListContainer() print("[UIManager] Interface base estruturada e pronta para populações dinâmicas.") -- Nota: A lógica de sincronização com DataModule e TeleportModule virá aqui. end -- UIManager.Initialize() -- Em um ambiente real, esta função seria executada após a injeção.

Neste estágio, o painel estava totalmente estruturado.

  1. Container Principal: Estilo Dark/Light, cantos arredondados, tamanho fixo.
  2. Header: Título e área de arrasto.
  3. Área de Controles (ControlArea):
    • Input de nome + Botão "Criar WP" com feedback visual de hover.
    • Seletores de método ("Instantâneo" / "Tween") claramente destacando o estado ativo (CurrentMethod = "instant").
  4. Lista de Waypoints: Um ScrollingFrame vazio, preparado para receber os itens.

Um pequeno deslize de design ocorreu. Ao criar a ControlArea, o UIListLayout foi anexado a ela e, em seguida, um ControlAreaPadding foi criado dentro. No entanto, para que o UIListLayout organize os elementos de controle (criação e seleção de método), eles precisavam ser filhos diretos da ControlArea, ou o Layout precisava ser movido para o frame interno.

A revisão rápida de código no CreateControlArea e na inicialização ajustou o layout, fazendo com que todos os componentes de controle primários fossem irmãos e geridos pelo mesmo UIListLayout, garantindo espaçamento e ordenação consistentes (1. Criação, 2. Separador, 3. Seletor de Método).

O fluxo de interação do usuário estava agora bem definido para esta parte do sistema:

A Decisão de Design sobre a Velocidade do Tween

Observe-se que o seletor de velocidade do Tween ainda não havia sido criado, como previsto pelo plano. O controle de velocidade só se torna relevante se o método de teleporte for tween. A filosofia de UX do sistema exigia que opções irrelevantes fossem ocultadas.

Portanto, o controle de velocidade do Tween (speed slider ou input) deveria ser um componente aninhado, que apareceria abaixo do TeleportMethodContainer e seria visível apenas quando CurrentMethod == "tween".

Em termos de arquitetura, isso adicionaria uma nova camada de complexidade em ControlArea:

  1. A ControlArea precisava ser capaz de redimensionar seu próprio Frame para acomodar o controle de velocidade adicional quando necessário.
  2. A lógica de SetMethodVisual precisaria ser expandida para controlar a visibilidade e a animação de aparição/desaparecimento desse novo frame.

Planejamento do Componente de Velocidade do Tween:

O controle de velocidade seria implementado como um Frame contendo um TextLabel e um TextBox para entrada numérica (mais preciso que um slider visualmente minimalista neste espaço compacto, embora um slider fosse uma boa opção futura).

local TweenSpeedControl = nil local function CreateTweenSpeedControl(parentFrame) TweenSpeedControl = Instance.new("Frame") TweenSpeedControl.Name = "SpeedControl" TweenSpeedControl.Size = UDim2.new(1, 0, 0, 30) TweenSpeedControl.BackgroundTransparency = 1 TweenSpeedControl.LayoutOrder = 4 -- Quarta posição na ControlArea TweenSpeedControl.Parent = parentFrame TweenSpeedControl.Visible = CurrentMethod == "tween" -- Inicialmente oculto se o padrão é instant -- Layout interno para o Label e o Input local ListLayout = Instance.new("UIListLayout") ListLayout.FillDirection = Enum.FillDirection.Horizontal ListLayout.Padding = UDim.new(0, 5) ListLayout.Parent = TweenSpeedControl ListLayout.VerticalAlignment = Enum.VerticalAlignment.Center -- Label local SpeedLabel = Instance.new("TextLabel") SpeedLabel.Text = "Velocidade (studs/s):" SpeedLabel.Size = UDim2.new(0.6, 0, 1, 0) SpeedLabel.BackgroundTransparency = 1 SpeedLabel.Font = GlobalStyle.Font SpeedLabel.TextSize = 13 SpeedLabel.TextColor3 = GlobalStyle.TextColor SpeedLabel.TextXAlignment = Enum.TextXAlignment.Left SpeedLabel.Parent = TweenSpeedControl -- Input Numérico local SpeedInput = Instance.new("TextBox") SpeedInput.Name = "SpeedInput" SpeedInput.Size = UDim2.new(0.4, -5, 1, 0) SpeedInput.BackgroundColor3 = GlobalStyle.AccentColor SpeedInput.BackgroundTransparency = 0.7 SpeedInput.BorderSizePixel = 0 SpeedInput.Text = "50" -- Valor padrão SpeedInput.TextSize = 14 SpeedInput.Font = GlobalStyle.Font SpeedInput.TextColor3 = GlobalStyle.TextColor SpeedInput.Parent = TweenSpeedControl -- Lógica de validação e salvamento do input de velocidade viria aqui SpeedInput.FocusLost:Connect(function(enterPressed) if enterPressed then local speed = tonumber(SpeedInput.Text) if speed and speed >= 10 and speed <= 500 then -- DataModule.SetSetting("tweenSpeed", speed) elseif speed < 10 then SpeedInput.Text = "10" elseif speed > 500 then SpeedInput.Text = "500" end end end) return TweenSpeedControl end

Atualização da Lógica de Mudança de Estado

O SetMethodVisual precisava agora controlar o novo componente e, o mais importante, ajustar o tamanho do ControlArea e a posição do WaypointListContainer. Uma mudança de tamanho abrupta seria ruim em UX; seria necessário um tween para suavizar essa transição.

O tamanho original da ControlArea (quando CurrentMethod == "instant") era de 100px. Com a adição do controle de velocidade (30px + padding), o tamanho total seria de aproximadamente 135px.

local function UpdateControlAreaHeight(isTweenActive) local newHeight = isTweenActive and 135 or 100 local controlAreaPosition = UDim2.new(0, 0, 0, GlobalStyle.HeaderHeight) -- 1. Tweening ControlArea Size TweenService:Create(ControlArea, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), { Size = UDim2.new(1, 0, 0, newHeight) }):Play() -- 2. Recalculando e Tweening a posição do WaypointListContainer local newYPosition = GlobalStyle.HeaderHeight + newHeight + GlobalStyle.Padding local remainingHeight = GlobalStyle.Size.Y.Offset - newYPosition - GlobalStyle.Padding TweenService:Create(WaypointListContainer, TweenInfo.new(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), { Position = UDim2.new(0, 0, 0, newYPosition), Size = UDim2.new(1, 0, 0, remainingHeight) }):Play() -- 3. Controlar visibilidade do componente de velocidade if TweenSpeedControl then TweenSpeedControl.Visible = isTweenActive end -- Forçar a atualização do layout ControlArea:FindFirstChildOfClass("UIListLayout"):ApplyLayoutChanges() end local function SetMethodVisual(method) -- ... (Lógica de cor e destaque dos botões permanece a mesma) local isTweenSelected = method == "tween" -- Inicia o tween de redimensionamento da ControlArea UpdateControlAreaHeight(isTweenSelected) end

A inclusão da lógica de tweening para o redimensionamento elevou a UX significativamente. Em vez de a lista de waypoints saltar repentinamente quando o usuário selecionasse o modo 'Tween', ela desceria com uma animação suave, revelando o controle de velocidade.

Finalizando a Estrutura da ControlArea

O componente de velocidade do Tween foi criado e inserido no fluxo da inicialização, após a criação inicial dos seletores de método.

-- Dentro da função UIManager.Initialize(), após CreateMethodSelect: -- ... -- Criação dos seletores de método CreateMethodSelect(ControlPaddingFrame) TeleportMethodContainer.LayoutOrder = 3 -- Criação do controle de velocidade (inicialmente visível=false se o método padrão for instantâneo) CreateTweenSpeedControl(ControlPaddingFrame) -- Necessário para o UIManager ter acesso ao serviço TweenService local TweenService = game:GetService("TweenService") -- Atualizar o SetMethodVisual para que use a lógica de UpdateControlAreaHeight -- (O código anterior já foi atualizado para referenciar essa nova lógica) -- É essencial garantir que o estado inicial (instantâneo) esteja configurado corretamente -- UpdateControlAreaHeight(false) -- Garante que a altura inicial seja 100px. -- Preparar a lista (ScrollFrame) local ScrollFrame = CreateWaypointListContainer() print("[UIManager] Interface base estruturada e pronta para populações dinâmicas. Controles de estado implementados.") -- Fim da UIManager.Initialize()

O Módulo UI estava agora em um estado onde os elementos estáticos e as decisões de UX de alto nível estavam concluídas. A arquitetura de estilos dark leve, transparência, cantos arredondados, e o controle de arrasto robusto estavam prontos. A área de controle abrigava as duas funcionalidades primárias (criação e seleção de modo), sendo a última visual e responsiva, animando sutilmente a transição entre os métodos de teleporte. A camada de lista de waypoints estava aguardando sua população dinâmica, que conectaria a UI ao DataModule para exibição e ao TeleportModule para ação.

O painel principal estava funcionalmente estruturado e posicionado, mas apenas com os botões estáticos de controle; a Lista de Waypoints e o seletor de velocidade do Tween ainda não estavam populados com lógica e precisam de interação dinâmica.

Comments (0)

No comments yet. Be the first to share your thoughts!

Sign In

Please sign in to continue.